import FloorsViewData from "../data/FloorsViewData"
import ViewmodeData from "../data/ViewmodeData"
import MeasurementInput from "../mesh/MeasurementInput"
import RaycasterModule from "../module/RaycasterModule"
import { removeArrayItem } from "../utils/array.util"
import ConstrainedHit from "../utils/ConstrainedHit"
import funcUtil from "../utils/func.util"
import { roomFunc } from "../utils/room.util"
import Le from "../math/13188"
import * as THREE from "three"
import Logger from "../utils/Logger"
import MockedRoom from "../three/MockedRoom"
import vectorsEnum from "../enum/vectors.enum"
import re from "../packages/60039"
import { measurementFeatureType, userFeatureType } from "../enum/measurement.enum"
import { Subscription } from "../utils/Subscription"
import SnappableAxisUpdater from "./SnappableAxisUpdater"
import SnapAxisLine3 from "../three/SnapAxisLine3"
const Ge = new Logger("snapping")

const Me = {
  [vectorsEnum.UP]: measurementFeatureType.AxisY,
  [vectorsEnum.DOWN]: measurementFeatureType.AxisY,
  [vectorsEnum.FORWARD]: measurementFeatureType.AxisZ,
  [vectorsEnum.BACK]: measurementFeatureType.AxisZ,
  [vectorsEnum.LEFT]: measurementFeatureType.AxisX,
  [vectorsEnum.RIGHT]: measurementFeatureType.AxisX,
  [vectorsEnum.HORIZONTAL_PLANE]: measurementFeatureType.AxisY,
  NONE: void 0
}

const Ie = {
  [measurementFeatureType.LinePoint]: userFeatureType.UserPoint,
  [measurementFeatureType.LineSegment]: userFeatureType.UserLine,
  [measurementFeatureType.AxisAny]: userFeatureType.UserAxis,
  [measurementFeatureType.AxisX]: userFeatureType.UserAxis,
  [measurementFeatureType.AxisY]: userFeatureType.UserAxis,
  [measurementFeatureType.AxisZ]: userFeatureType.UserAxis,
  [measurementFeatureType.Mesh]: userFeatureType.ModelFeature,
  [measurementFeatureType.RoomMesh]: userFeatureType.ModelFeature,
  [measurementFeatureType.SnapLine]: userFeatureType.ModelFeature,
  [measurementFeatureType.SnapPoint]: userFeatureType.ModelFeature
}

const Re = (e: THREE.Intersection) => e && "face" in e && void 0 !== e.face && roomFunc.hasFloorId(e.object)
const Ee = (() => {
  return (e, t, i, s, n) => ({
    point: e,
    object: new MockedRoom(s, n),
    face: { a: 0, b: 1, c: 2, normal: t, materialIndex: 0 },
    distance: i
  })
})()
export default class SnappablePointer {
  raycaster: RaycasterModule
  getConstraintStyle: () => number
  floorsViewData: FloorsViewData
  viewmodeData: ViewmodeData
  origin: ConstrainedHit
  planeNormal: THREE.Vector3
  plane: THREE.Plane
  ray: THREE.Ray
  originChangedListeners: any[]
  registeredSnapFeatures: Record<number, Array<{ owner: SnappableAxisUpdater; features: SnapAxisLine3[] }>>
  clearOrigin: () => void
  setOrigin: (e: ConstrainedHit, t?: boolean) => void
  setOriginFromPointer: (e: any) => void
  snapFeatures: (e: any) => any
  addSnapFeatures: (e: SnappableAxisUpdater, t: number, i: SnapAxisLine3[]) => void
  removeSnapFeatures: (e: SnappableAxisUpdater, t: number) => void
  filters: { nop: () => boolean; userPoints: (e: any) => boolean; userLines: (e: any) => boolean; userFeatures: (e: any) => any }
  meshSnapRadius: number
  cachedHit: ConstrainedHit
  constructor(e, t, i, s, n) {
    this.raycaster = e
    this.getConstraintStyle = t
    this.floorsViewData = s
    this.viewmodeData = n
    this.origin = null
    this.planeNormal = new THREE.Vector3()
    this.plane = new THREE.Plane()
    this.ray = new THREE.Ray()
    this.originChangedListeners = []
    this.registeredSnapFeatures = {
      [userFeatureType.UserAxis]: [],
      [userFeatureType.UserLine]: [],
      [userFeatureType.UserPoint]: [],
      [userFeatureType.ModelFeature]: []
    }
    this.clearOrigin = () => {
      this.origin = null
      this.originChangedListeners.forEach(e => e(null))
    }
    this.setOrigin = (e, t = !1) => {
      ;(this.origin && !t) ||
        ((this.origin = e),
        this.plane.setFromNormalAndCoplanarPoint(this.origin.normal, this.origin.point),
        this.originChangedListeners.forEach(e => e(this.origin)),
        Ge.debug("Updating origin", this.origin, { forceUpdate: t }))
    }
    this.setOriginFromPointer = e => {
      if (this.origin) return
      const t = this.getMeshIntersection()
      if (!t) return
      const i = new ConstrainedHit(t, this.getConstraintStyle())
      e && i.updatePoint(e)
      this.setOrigin(i, !0)
    }
    this.snapFeatures = e => this.registeredSnapFeatures[e].reduce((e, t) => e.concat(t.features), [])
    this.addSnapFeatures = (e, t, i) => {
      const s = Ie[t]
      this.registeredSnapFeatures[s].push({ owner: e, features: i })
      Ge.debug(`Adding ${i.length} snap feature groups`, measurementFeatureType[t], t)
    }
    this.removeSnapFeatures = (e, t) => {
      const i = Ie[t]
      const s = this.registeredSnapFeatures[i].findIndex(t => t.owner === e)
      if (-1 !== s) {
        const e = this.registeredSnapFeatures[i].splice(s, 1)
        Ge.debug(`Removing ${e.length} snap feature groups`, measurementFeatureType[t], t)
      } else Ge.debug(`removeTemporarySnapFeature: ${t} ${measurementFeatureType[t]} not found from`, e, this.registeredSnapFeatures)
    }
    this.filters = {
      nop: () => !0,
      userPoints: e => {
        if (void 0 === e.object) return !1
        if ((t = e.object) && t instanceof THREE.Vector3) for (const t of this.snapFeatures(userFeatureType.UserPoint)) if (t.equals(e.object)) return !0
        var t
        return !1
      },
      userLines: e => {
        if (void 0 === e.object) return !1
        if (
          ((t = e.object) && "isSnapAxisLine3" in t) ||
          "isSnapUserLine3" in t ||
          "isSnapLine3" in t ||
          ("start" in t && "end" in t && "closestPointToPoint" in t)
        ) {
          for (const t of this.snapFeatures(userFeatureType.UserLine)) if (t.equals(e.object)) return !0
          for (const t of this.snapFeatures(userFeatureType.UserAxis)) if (t.equals(e.object)) return !0
        }
        var t
        return !1
      },
      userFeatures: e => this.filters.userPoints(e) || this.filters.userLines(e)
    }
    this.meshSnapRadius = i ? re.Oq.mobile : re.Oq.desktop
  }
  preload() {
    this.raycaster.snapping.preloadMeshSnapping()
  }
  getMeshIntersection(e?) {
    let t: THREE.Intersection[]
    let i: THREE.Intersection = null
    let s = null
    if (e && e["origin"] && e["normal"]) {
      s = !0
      t = this.raycaster.picking.cast(e["origin"], e.normal)
    } else {
      t = this.raycaster.pointer.cast()
    }
    i = t[0]
    if (this.viewmodeData.isFloorplan() && i && !s) {
      i = t.filter(e => !(e.object instanceof MeasurementInput))[0]
      const e = this.floorsViewData.getHighestVisibleFloor()
      const s = roomFunc.matchesFloorId(e.id)
      !(!i || (Re(i) && s(i.object))) && (i = Ee(i.point, new THREE.Vector3(0, 1, 0), i.distance, e.id, null)),
        (i.point.y = e.boundingBox.max.y),
        i.face && i.face.normal.set(0, 1, 0)
    } else {
      i = t.filter(e => funcUtil.raycastVisible(e.object))[0]
    }
    return Re(i) ? i : null
  }
  getIntersection(e?) {
    const t = this.getMeshIntersection(e)
    if (!t) return null
    this.cachedHit || (this.cachedHit = new ConstrainedHit(t, this.getConstraintStyle()))
    const i = this.cachedHit.updateFromIntersection(t, this.getConstraintStyle())
    const s = this.raycaster.pointer.pointerRay
    this.ray.set(s.origin, s.direction)
    this.planeNormal.copy(i.normal)
    switch (this.getConstraintStyle()) {
      case Le.l1.Free:
        return i
      case Le.l1.Axes:
        return this.lockToWorldAxes(i)
      case Le.l1.PlanarAxes:
        const e = [
          ...this.snapFeatures(userFeatureType.UserAxis),
          ...this.snapFeatures(userFeatureType.UserLine),
          ...this.snapFeatures(userFeatureType.UserPoint)
        ]
        return this.softSnapToEdges(i, e, this.filters.userFeatures)
      case Le.l1.Edges:
        const t = [...this.snapFeatures(userFeatureType.UserLine), ...this.snapFeatures(userFeatureType.UserPoint)]
        return this.softSnapToEdges(i, t, this.filters.nop)
      case Le.l1.EdgesAndPlanarAxes:
        const s = [
          ...this.snapFeatures(userFeatureType.UserAxis),
          ...this.snapFeatures(userFeatureType.UserLine),
          ...this.snapFeatures(userFeatureType.UserPoint)
        ]
        return this.softSnapToEdges(i, s, this.filters.nop)
    }
    return i
  }
  get lastIntersection() {
    return this.cachedHit
  }
  onOriginChanged(e, t = !1) {
    const i = {
      renew: () => {
        this.originChangedListeners.push(e)
      },
      cancel: () => {
        removeArrayItem(this.originChangedListeners, e)
      }
    }
    t && i.renew()
    return i as Subscription
  }
  lockToWorldAxes(e) {
    if (this.origin) {
      const t = Le.r2(this.origin.point, e.point, Le.l1.Axes)
      this.cachedHit.copy(e)
      this.cachedHit.point.copy(t.position)
      this.cachedHit.constraint = "NONE" !== t.axisName ? Le.l1.Axes : Le.l1.Free
      this.cachedHit.featureType = Me[t.axisName]
      return this.cachedHit
    }
    return e
  }
  softSnapToEdges(e: ConstrainedHit, t, i) {
    const s = this.origin ? this.origin.point : e.point
    const n = this.origin ? this.origin.normal : e.normal
    this.raycaster.snapping.add(...t)
    const r = this.raycaster.snapping.cast(this.ray, this.meshSnapRadius, s, n).filter(i)
    const o = this.raycaster.snapping.pick(this.ray, this.meshSnapRadius, r)
    return this.raycaster.snapping.remove(...t), o ? (this.cachedHit.updateFromSnapIntersection(o, this.getConstraintStyle(), n), this.cachedHit) : e
  }
}
